﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Utility;

namespace BlizzardApi.Services
{
    public interface IRequestCache
    {
        CacheResult Find(Uri uri);
        void Cache(Uri uri, DateTime? lastModified, DateTime? expires, string eTag, Stream stream);
    }

    public class CacheResult
    {
        public readonly Stream Stream;
        public readonly DateTime? LastModified;
        public readonly DateTime? Expires;
        public readonly string ETag;

        public CacheResult(Stream stream, DateTime? lastModified, DateTime? expires, string eTag)
        {
            Stream = stream;
            LastModified = lastModified;
            Expires = expires;
            ETag = eTag;
        }
    }

    public class RequestCache : IRequestCache, IDisposable
    {
        private static readonly IDictionary<string, FileCache> RequestFileMap;

        private const string CacheFolder = "Cache";
        private readonly static string CacheIndexFile = Path.Combine(CacheFolder, "index.csv");

        static RequestCache()
        {
            if (!Directory.Exists(CacheFolder))
                Directory.CreateDirectory(CacheFolder);

            IList<IEnumerable<string>> data = new List<IEnumerable<string>>();
            if (File.Exists(CacheIndexFile))
                using (var reader = new StreamReader(CacheIndexFile))
                {
                    reader.ReadLine();
                    while (!reader.EndOfStream)
                        data.Add(reader.ReadLine().ParseCsv());
                }
            RequestFileMap = data.Select(x => FileCache.Parse(x)).ToDictionary(x => x.Key);

            var toDelete = Directory.GetFiles(CacheFolder, "*.res").Where(theFile => !RequestFileMap.Any(x => x.Value.FileName == theFile)).ToList();
            foreach (var file in toDelete)
                File.Delete(file);
        }

        ~RequestCache()
        {
            Flush();
        }

        public void Dispose()
        {
            Flush();
        }

        private static void Flush()
        {
            if (File.Exists(CacheIndexFile))
                File.Delete(CacheIndexFile);
            var csvStream = new[] {FileCache.Headers}.Concat(RequestFileMap.Values.OrderBy(x => x.Key).Select(x => x.Serialize())).ToCsv();
            using (csvStream)
            using (var fs = File.OpenWrite(CacheIndexFile))
                csvStream.CopyTo(fs);
        }

        public CacheResult Find(Uri uri)
        {
            var key = ToKey(uri);
            FileCache fileCache;
            if (RequestFileMap.TryGetValue(key, out fileCache) && File.Exists(fileCache.FileName))
                return new CacheResult(File.OpenRead(fileCache.FileName), fileCache.LastModified, fileCache.Expires, fileCache.ETag);
            return null;
        }

        public void Cache(Uri uri, DateTime? lastModified, DateTime? expires, String eTag, Stream stream)
        {
            var key = ToKey(uri);
            FileCache fileCache;
            if (RequestFileMap.TryGetValue(key, out fileCache) && File.Exists(fileCache.FileName))
            {
                fileCache.LastModified = lastModified;
                fileCache.Expires = expires;
                fileCache.ETag = eTag;
            }
            else
            {
                var fileName = Path.Combine(CacheFolder, Guid.NewGuid().ToString("n") + ".res");
                using (var fs = File.OpenWrite(fileName))
                    stream.CopyTo(fs);
                stream.Rewind();
                fileCache = new FileCache(key, lastModified, expires, eTag, fileName);
            }
            RequestFileMap[key] = fileCache;
        }

        private static string ToKey(Uri uri)
        {
            return uri.ToString();
        }

        private class FileCache
        {
            public readonly string Key;
            public readonly string FileName;
            public string ETag;
            public DateTime? LastModified;
            public DateTime? Expires;

            public FileCache(string key, DateTime? lastModified, DateTime? expires, string eTag, string fileName)
            {
                Key = key;
                LastModified = lastModified;
                Expires = expires;
                FileName = fileName;
                ETag = eTag;
            }

            public static FileCache Parse(IEnumerable<string> data)
            {
                var lastModified = data.ElementAt(1).ToNullableDateTime();
                var expires = data.ElementAt(2).ToNullableDateTime();
                if (lastModified.HasValue)
                    lastModified = lastModified.Value.ToUniversalTime();
                if (expires.HasValue)
                    expires = expires.Value.ToUniversalTime();
                return new FileCache(data.First(), lastModified, expires, data.ElementAt(3), data.Last());
            }

            public static readonly IEnumerable<string> Headers = new[] { "Key", "Last Modified", "Expires", "ETag", "File" };


            public IEnumerable<string> Serialize()
            {
                yield return Key;
                yield return LastModified.HasValue ? LastModified.Value.ToString("u") : string.Empty;
                yield return Expires.HasValue ? Expires.Value.ToString("u") : string.Empty;
                yield return ETag;
                yield return FileName;
            }
        }
    }
}